package com.google.gwt.gfx.client;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.gfx.client.GFXPanel.Command;
import com.google.gwt.gfx.client.layers.Layer;
import com.google.gwt.gfx.client.overlays.AnchorOverlay;
import com.google.gwt.gfx.client.overlays.Overlay;
import com.google.gwt.gfx.client.services.Refreshable;

/**
 * A type of {@link Command} provides panning functionalities of a
 * {@link GFXPanel}.
 */
public class PanCommand<B extends GFXPanel<?, ?, ?>> implements Command<B> {

	public static interface Callback {

		void onPan();

	}

	private class PanAnimation extends Animation {

		private Callback callback;

		private int left;

		private int top;

		public PanAnimation(int left, int top, Callback callback) {
			this.left = left;
			this.top = top;
			this.callback = callback;
		}

		@Override
		protected void onComplete() {
			super.onComplete();

			endPan();

			callback.onPan();
		}

		@Override
		protected void onStart() {
			beginPan();

			super.onStart();
		}

		@Override
		protected void onUpdate(double progress) {
			continuePan((int) (left * progress), (int) (top * progress));
		}

	}

	/**
	 * Default {@link #getAutoRefreshThreshold()}.
	 */
	public static final int DEFAULT_AUTO_REFRESH_THRESHOLD = 0;

	public static final double DEFAULT_HEIGHT_THRESHOLD = 0.6;

	/**
	 * Default {@link #getPanMillis()}.
	 */
	public static final int DEFAULT_PAN_MILLIS = 1000;

	public static final double DEFAULT_WIDTH_THRESHOLD = 0.6;

	public static Callback IDLE = new Callback() {

		@Override
		public void onPan() {

		}

	};

	private boolean autoRefresh = false;

	private int autoRefreshThreshold = DEFAULT_AUTO_REFRESH_THRESHOLD;

	private B buddyRef;

	private double heightThreshold = DEFAULT_HEIGHT_THRESHOLD;

	private int panLeftStart = 0;

	private int panMillis = DEFAULT_PAN_MILLIS;

	private int panTopStart = 0;

	private int prevPanHeight = 0;

	private int prevPanWidth = 0;

	private double widthThreshold = DEFAULT_WIDTH_THRESHOLD;

	/**
	 * @see #endMove()
	 */
	public void beginPan() {
		prevPanWidth = 0;
		prevPanHeight = 0;
	}

	/**
	 * Begins panning using {@link beginPan()}. Before
	 * {@link #continuePanTo(int, int)} and {@link #endPan()}.
	 * 
	 * @param panLeft
	 *            position of panning along x-axis
	 * @param panTop
	 *            position of panning along y-axis
	 */
	public void beginPanTo(int panLeft, int panTop) {
		panLeftStart = panLeft;
		panTopStart = panTop;

		beginPan();
	}

	public void bind(final B buddy) {
		buddyRef = buddy;
	}

	public B buddy() {
		return buddyRef;
	}

	/**
	 * Sets layer offsets of this viewer without {@link GFXPanel#refresh()}.
	 * 
	 * @param panWidth
	 *            Offsets along x-axis from movement origin.
	 * @param panHeight
	 *            Offsets along y-axis from movement origin.
	 */
	public void continuePan(int panWidth, int panHeight) {
		for (Layer<?> layer : buddy().layers()) {
			if (layer.style().isVisible()) {
				buddy().content().setWidgetPosition(
						layer,
						panWidth - prevPanWidth
								+ buddy().content().getWidgetLeft(layer),
						panHeight - prevPanHeight
								+ buddy().content().getWidgetTop(layer));
			}
		}

		for (Overlay overlay : buddy().overlays()) {
			// TODO check bounds
			if (overlay instanceof Refreshable<?>
					&& overlay.style().isVisible()) {
				overlay.style().setLeft(
						panWidth - prevPanWidth + overlay.style().getLeft());
				overlay.style().setTop(
						panHeight - prevPanHeight + overlay.style().getTop());
			}
		}

		prevPanWidth = panWidth;
		prevPanHeight = panHeight;
	}

	/**
	 * Continues panning using {@link continuePan(int, int)}. After
	 * {@link #beginPanTo(int,int)} and before {@link #endPan()}.
	 * 
	 * @param panLeft
	 *            position of panning along x-axis
	 * @param panTop
	 *            position of panning along y-axis
	 */
	public void continuePanTo(int panLeft, int panTop) {
		int moveLeft = panLeft - panLeftStart;
		int moveTop = panTop - panTopStart;

		continuePan(moveLeft, moveTop);

		if (autoRefresh) {
			if (Math.abs(moveLeft) > autoRefreshThreshold
					|| Math.abs(moveTop) > autoRefreshThreshold) {
				endPan();
				beginPanTo(panLeft, panTop);
			}
		}
	}

	/**
	 * Ends panning using {@link MapViewer#moveBackAndRefresh()}. After
	 * {@link #beginPanTo(int, int)} and {@link #continuePanTo(int, int)}.
	 */
	public void endPan() {
		buddyRef.pan(prevPanWidth, prevPanHeight);

		prevPanWidth = 0;
		prevPanHeight = 0;
	}

	/**
	 * @return if {@link #isAutoRefresh()} and distance of panning is more than
	 *         this threshold, {@link #refresh()} will be executed.
	 */
	public int getAutoRefreshThreshold() {
		return autoRefreshThreshold;
	}

	public double getHeightThreshold() {
		return heightThreshold;
	}

	public int getPaddingHeight() {
		return (int) (buddy().viewport().getHeight() * heightThreshold);
	}

	public int getPaddingWidth() {
		return (int) (buddy().viewport().getWidth() * widthThreshold);
	}

	/**
	 * @return time duration in milliseconds of one pan
	 */
	public int getPanMillis() {
		return panMillis;
	}

	public double getWidthThreshold() {
		return widthThreshold;
	}

	/**
	 * @return true if {@link #refresh()} will be executed when distance of
	 *         panning is more than {@link #getAutoRefreshThreshold()}.
	 */
	public boolean isAutoRefresh() {
		return autoRefresh;
	}

	public void pan(int panLeft, int panTop) {
		pan(panLeft, panTop, IDLE);
	}

	/**
	 * Pans the viewer using an animation.
	 * 
	 * @param panLeft
	 *            total distance of panning along x-axis
	 * @param panTop
	 *            total distance of panning along y-axis
	 * @param millis
	 *            the duration of the animation in milliseconds
	 */
	public void pan(int panLeft, int panTop, Callback callback) {
		PanAnimation animation = new PanAnimation(panLeft, panTop, callback);
		animation.cancel();
		animation.run(panMillis);
	}

	public void pan(int panLeft, int panTop, int millis) {
		pan(panLeft, panTop, millis, IDLE);
	}

	/**
	 * Pans the viewer using an animation.
	 * 
	 * @param panLeft
	 *            total distance of panning along x-axis
	 * @param panTop
	 *            total distance of panning along y-axis
	 * @param millis
	 *            the duration of the animation in milliseconds
	 */
	public void pan(int panLeft, int panTop, int millis, Callback callback) {
		PanAnimation animation = new PanAnimation(panLeft, panTop, callback);
		animation.cancel();
		animation.run(millis);
	}

	public void panFrom(AnchorOverlay pin) {
		panFrom(pin, false, panMillis);
	}

	public void panFrom(final AnchorOverlay pin, final boolean resizable,
			int millis) {
		int centerLeft = pin.style().getLeft() + pin.style().getWidth() / 2;
		int centerTop = pin.style().getTop() + pin.style().getHeight() / 2;
		int width = pin.style().getWidth();
		int height = pin.style().getHeight();

		int boardWidth = buddy().viewport().getWidth();
		int boardHeight = buddy().viewport().getHeight();
		int maxWidth = getPaddingWidth();
		int maxHeight = getPaddingHeight();

		if (centerLeft >= width / 2 && centerLeft + width / 2 <= boardWidth
				&& centerTop >= height / 2
				&& centerTop + height / 2 <= boardHeight) {
			buddy().refresh();
			return;
		}

		int panLeft = 0;
		int panTop = 0;

		if (width > maxWidth) {
			panLeft = boardWidth / 2 - centerLeft;
		} else if (centerLeft + width / 2 > boardWidth) {
			panLeft = boardWidth / 2 + maxWidth / 2 - centerLeft - width / 2;
		} else if (centerLeft - width / 2 < 0) {
			panLeft = boardWidth / 2 - maxWidth / 2 - centerLeft + width / 2;
		}

		if (height > maxHeight) {
			panTop = boardHeight / 2 - centerTop;
		} else if (centerTop + height / 2 > boardHeight) {
			panTop = boardHeight / 2 + maxHeight / 2 - centerTop - height / 2;
		} else if (centerTop - height / 2 < 0) {
			panTop = boardHeight / 2 - maxHeight / 2 - centerTop + height / 2;
		}

		pan(panLeft, panTop, millis, new Callback() {

			@Override
			public void onPan() {
				if (resizable) {
					if (pin.style().getWidth() > getPaddingWidth()) {
						pin.style().setWidth(getPaddingWidth());
					}
					if (pin.style().getHeight() > getPaddingHeight()) {
						pin.style().setHeight(getPaddingHeight());
					}
				}
			}

		});

	}

	/**
	 * Pans once.
	 * 
	 * @param panLeft
	 *            total distance of panning along x-axis
	 * @param panTop
	 *            total distance of panning along y-axis
	 */
	public void panTo(int panLeft, int panTop) {
		beginPanTo(0, 0);
		continuePanTo(panLeft, panTop);
		endPan();
	}

	public void panToCenterFrom(AnchorOverlay pin) {
		panToCenterFrom(pin, false, panMillis);
	}

	public void panToCenterFrom(final AnchorOverlay pin,
			final boolean resizable, int millis) {
		int centerLeft = pin.style().getLeft() + pin.style().getWidth() / 2;
		int centerTop = pin.style().getTop() + pin.style().getHeight() / 2;
		int width = pin.style().getWidth();
		int height = pin.style().getHeight();

		int boardWidth = buddy().viewport().getWidth();
		int boardHeight = buddy().viewport().getHeight();

		if (centerLeft >= width / 2 && centerLeft + width / 2 <= boardWidth
				&& centerTop >= height / 2
				&& centerTop + height / 2 <= boardHeight) {
			buddy().refresh();
			return;
		}

		int panLeft = 0;
		int panTop = 0;

		panLeft = boardWidth / 2 - centerLeft;
		panTop = boardHeight / 2 - centerTop;
		pan(panLeft, panTop, millis, new Callback() {

			@Override
			public void onPan() {
				if (resizable) {
					if (pin.style().getWidth() > getPaddingWidth()) {
						pin.style().setWidth(getPaddingWidth());
					}
					if (pin.style().getHeight() > getPaddingHeight()) {
						pin.style().setHeight(getPaddingHeight());
					}
				}
			}

		});
	}

	/**
	 * @param autoRefresh
	 *            {@link #isAutoRefresh()}
	 */
	public void setAutoRefresh(boolean autoRefresh) {
		this.autoRefresh = autoRefresh;
	}

	/**
	 * @param autoRefreshThreshold
	 *            {@link #getAutoRefreshThreshold()}
	 */
	public void setAutoRefreshThreshold(int autoRefreshThreshold) {
		this.autoRefreshThreshold = autoRefreshThreshold;
	}

	public void setHeightThreshold(double heightThreshold) {
		this.heightThreshold = heightThreshold;
	}

	/**
	 * @param panMillis
	 *            {@link #getPanMillis()}
	 */
	public void setPanMillis(int panMillis) {
		this.panMillis = panMillis;
	}

	public void setWidthThreshold(double widthThreshold) {
		this.widthThreshold = widthThreshold;
	}

}
